---
title: Patrol finders - advanced
---

We aim to make Patrol as simple as possible, but there are still a few matters
that we feel require some more attention. We'll explain them in this section.

### How is Patrol's `tap()` different from Flutter's `tap()`?

Let's consider this test, written without Patrol:

```dart
await tester.tap(find.byKey(Key('addComment')).first);
await tester.pumpAndSettle();
```

This code:

1. Immediately atttempts to find the first widget with the `addComment` key
2. After finding the widget, it immediately attempts to tap on it

This is the default behavior, but in our experience, it's often a source of
flakiness. For example, the widget having `addComment` key might not be visible
at the time when the finder is run. This usually doesn't means that the test
should fail. Probably an HTTP request was made to fetch the post, and when the
fetching is done, the widget having `addComment` key will show up.

To achieve this behavior, you'd have to do:

```dart
while (find.byKey(Key('addComment')).first.evaluate().isEmpty) {
  await tester.pump(Duration(milliseconds: 100));
}

await tester.tap(find.byKey(Key('addComment')).first);
await tester.pumpAndSettle();
```

Our tiny example got really big, but it's still got two problems.

1.  If something goes wrong and `addComment` never shows up, we'll keep waiting
    indefinitely.

2.  The widget with `addComment` key might be present in the widget tree, but
    still not be visible to the user. By default, Flutter's default
    `WidgetTester` doesn't care. This is almost never desirable.

Fortunately, you don't have to overcome these problems. Patrol already did it!

Below is the same test, with all the above problems fixed, written with Patrol's
custom finders:

```dart
await $(#addComment).tap();
```

This code:

1. Attempts to find the first widget with `addComment` that is visible on
   screen. If it's not found immediately, it keeps trying until it finds it, or
   throws an exception if timeout.
2. Taps on it.

The timeout can be configured globally:

```dart
patrolWidgetTest(
  'logs in successfully',
  config: PatrolTestConfig(findTimeout: Duration(seconds: 10)),
  ($) async {
  // your test code
  },
);
```

You can also change the timeout ad-hoc:

```dart
await $(#addComment).tap(findTimeout: Duration(seconds: 30));
```

### You gotta pump it up! But which one to use?

In Flutter, "pumping" means rendering frames to the screen.

If there are no frames to pump, no animations are pending, which usually means
that the next action during the test can be executed. It is an equivalent of
what a human tester would do while testing an app - they would wait until the
app's state stabilizes after they've done something. For example, they tap on a
button and get redirected to another screen, but the data that will be shown
there hasn't been loaded yet. In such a case, a human tester waits until a
loader (or other animation) finishes. Pumping mechanism does exactly this - it
renders consecutive frames on the screen. For how long, exactly? Usually, we
want to pump frames as long as they come. That's what [pumpAndSettle()] does.

`pumpAndSettle()` method is called by default inside all actions that can be
performed while testing - tapping, scrolling, entering text, and so on. You can
change that by setting the `settlePolicy` argument:

```dart
await $('Delete account').tap(settlePolicy: SettlePolicy.settle);
await $('Confirm').tap(settlePolicy: SettlePolicy.pump);
```

`SettlePolicy` is an `enum` with 3 values. The default is `SettlePolicy.settle`
but you can change it to `pump` or `trySettle`. Those values map to methods like this:
 - `noSettle` -> `pump()`,
 - `trySettle` -> `pumpAndTrySettle()`,
 - `settle` -> `pumpAndSettle()`.

While `settle` and `pump` simply refer to Flutter's built-in methods,
`trySettle` is available only in Patrol. How is it different from other ones?

`pumpAndTrySettle()` is pretty much like `pumpAndSettle()`, the only difference
is that `pumpAndSettle()` throws an exception, if there were still new frames to
render after sonme defined timeout, while `pumpAndTrySettle()` does not. That's
why it has "`try`" in it's name.

When to use this new pumping method? Let us picture a scenario, in which we have
to deal with some animations. Let's say, that your app has some endless
animations, e.g. on a homescreen, to keep user's attention. You'd like to wait
for some things to happen, but using `pumpAndSettle`, you'll keep getting an
exception, because after some time, defined by `timeout`, there will be still
new frames to render. On the other hand, you still want to pump frames for some
time - if you didn't, the screen you want to interact with might not be rendered
yet, or it would have some widgets missing or data not yet loaded.

So, we decided to add a way to try settle - pump frames for some time (10
seconds by default), but if after that time there is still something new to
render - do nothing and continue the test.

We recommend using `pumpAndTrySettle()`, because it works with both kinds of
animations - finite and infinite. This settle policy will be new default in
future Patrol releases.

### How does `scrollTo()` work?

The `scrollTo()` method is simple to use, yet very powerful. Here's how you use
it to scroll to and tap on the first widget with the `"Delete account"` text:

```dart
await $('Delete account').scrollTo().tap();
```

And here's how `scrollTo()` works:

1. Waits for at least 1 [Scrollable] widget (or whatever you provided in 
   `view` argument) to become visible
2. Scrolls this widget in its scrolling direction until the target
   widget becomes visible
3. If the target widget becomes visible within timeout, it finishes, otherwise
   it throws an exception

Most of the time, you use `scrollTo()` and it just works, but there's 1
important thing to keep in mind when using `scrollTo()`:

**`scrollTo()`, by default, scrolls the first `Scrollable` widget**

This default is reasonable and what you want most of the time. Unfortunately,
this behavior can sometimes cause problems in more complicated UIs, where more
than a single `Scrollable` widget is visible at the same time. In such cases we
strongly recommend explicitly specifying the `view` that `scrollTo()`
should scroll, to avoid the problem of the target widget never becoming visible
because the wrong widget was scrolled.

To demonstrate this problem, let's consider this very simple app:

```dart
class App extends StatelessWidget {
  App({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Column(
          children: [
            Expanded(child: ListView(key: Key('listView1'))),
            Expanded(
              child: ListView.builder(
                key: Key('listView2'),
                itemCount: 101,
                itemBuilder: (context, index) => Text('index: $index'),
              ),
            ),
          ],
        ),
      ),
    );
  }
}
```

Now let's say that you're writing a test and want to scroll to and tap on the
first widget with the `"index: 100"` text (that is the last `Text` widget built
by the second `ListView` widget):

There's a high chance that you'd write this:

```dart
await $('index: 100').scrollTo().tap();
```

Unfortunately, running this test gives the `pumpAndSettle timed out` error.
That's because the `scrollTo()` was trying to scroll the first visible
`Scrollable` widget, which happens to be the first `ListView` (the one with
`listView1` key and no children).

To fix this problem, you have to explicitly specify which `Scrollable` you want
to use:

```dart
await $('index: 100').scrollTo(view: $(#listView2).$(Scrollable)).tap();
```

The above snippet will scroll the second `Scrollable` and find the widget with
`"index: 100"` text.

**Why so verbose?**

You might be wondering why `scrollTo(view: $(#listView2))` is not enough?
Why is it needed to look for a `Scrollable` widget inside the widget with the
`listView2` key?

This is because the [ListView] widget doesn't extend [Scrollable] – instead, it
builds a subclass of [Scrollable] itself. [This is a known Flutter
problem](https://github.com/flutter/flutter/issues/88762).

[listview]: https://api.flutter.dev/flutter/widgets/ListView-class.html
[scrollable]: https://api.flutter.dev/flutter/widgets/Scrollable-class.html
[pumpAndSettle()]: https://api.flutter.dev/flutter/flutter_test/WidgetTester/pumpAndSettle.html
